﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Reflection;
using System.Reflection.Emit;

namespace TfsReports.Web.Helpers
{
    public class Data
    {
        /// <summary>
        /// Gets the data reader.
        /// </summary>
        /// <param name="connectionStringKey">The connection string key.</param>
        /// <param name="storedProcedure">The stored procedure.</param>
        /// <param name="parameters">The parameters.</param>
        /// <returns>A data reader.</returns>
        public static IDataReader GetDataReader(string connectionStringKey, string storedProcedure, List<Parameter> parameters)
        {
            SqlCommand command = GetCommandWithConnection(connectionStringKey);
            command.CommandText = storedProcedure;
            command.CommandType = storedProcedure.ToUpper().StartsWith("SELECT ")
                ? CommandType.Text
                : CommandType.StoredProcedure;
            if (parameters != null)
                parameters.ForEach(p => command.Parameters.AddWithValue(p.Key, p.Value));
            command.Connection.Open();
            return command.ExecuteReader(CommandBehavior.CloseConnection);
        }

        /// <summary>
        /// Gets the connection string.
        /// </summary>
        /// <param name="connectionStringKey">The connection string key.</param>
        /// <returns>The connection string identified by the specified key.</returns>
        private static string GetConnectionString(string connectionStringKey)
        {
            return ConfigurationManager.ConnectionStrings[connectionStringKey].ToString();
        }

        /// <summary>
        /// Gets the command with connection.
        /// </summary>
        /// <param name="connectionStringKey">The connection string key.</param>
        /// <returns>An SQL connection.</returns>
        private static SqlCommand GetCommandWithConnection(string connectionStringKey)
        {
            return new SqlConnection(GetConnectionString(connectionStringKey)).CreateCommand();
        }
    }

    /// <summary>
    /// Parameter
    /// </summary>
    public class Parameter
    {
        /// <summary>
        /// Gets or sets the key.
        /// </summary>
        /// <value>The key.</value>
        public string Key { get; private set; }

        /// <summary>
        /// Gets or sets the value.
        /// </summary>
        /// <value>The value.</value>
        public object Value { get; protected internal set; }

        /// <summary>
        /// Gets or sets the direction.
        /// </summary>
        /// <value>The direction.</value>
        public ParameterDirection Direction { get; protected internal set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="Parameter"/> class.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="value">The value.</param>
        public Parameter(string key, object value) : this(key, value, ParameterDirection.Input) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="Parameter"/> class.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="value">The value.</param>
        /// <param name="direction">The direction.</param>
        public Parameter(string key, object value, ParameterDirection direction)
        {
            Key = key;
            Value = value;
            Direction = direction;
        }
    }

    /// <summary>
    /// Parameters
    /// </summary>
    public class Parameters : List<Parameter>
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="Parameters"/> class.
        /// </summary>
        public Parameters() { }

        /// <summary>
        /// Initializes a new instance of the <see cref="Parameters"/> class.
        /// </summary>
        /// <param name="o">The o.</param>
        public Parameters(object o)
        {
            Populate(o);
        }

        /// <summary>
        /// Adds the specified key.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="value">The value.</param>
        /// <exception cref="Exception" />
        public void Add(string key, object value)
        {
            if (ContainsKey(key))
                throw new Exception("Parameter with the specified key already exists.");
            Add(new Parameter(key, value));
        }

        /// <summary>
        /// Adds the specified key.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="value">The value.</param>
        /// <param name="direction">The direction.</param>
        public void Add(string key, object value, ParameterDirection direction)
        {
            if (ContainsKey(key))
                throw new Exception("Parameter with the specified key already exists.");
            Add(new Parameter(key, value, direction));
        }

        /// <summary>
        /// Determines whether the instance contains the specified key.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <returns>
        /// 	<c>true</c> if the instance contains the specified key; otherwise, <c>false</c>.
        /// </returns>
        public bool ContainsKey(string key)
        {
            return (Find(p => p.Key == key) != null);
        }

        /// <summary>
        /// Gets the index of the parameter with the specified key.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <returns>The index of the parameter with the specified key</returns>
        /// <exception cref="IndexOutOfRangeException" />
        protected internal int GetIndex(string key)
        {
            int? index = null;
            for (int i = 0; i < Count; i++)
            {
                if (this[i].Key == key)
                {
                    index = i;
                    break;
                }
            }
            if (index == null)
                throw new IndexOutOfRangeException("Parameter with the specified key does not exist.");
            return (int)index;
        }

        /// <summary>
        /// Gets or sets the Value property of the <see cref="Parameter"/> with the specified key.
        /// </summary>
        /// <value></value>
        public object this[string key]
        {
            get { return this[GetIndex(key)].Value; }
            set { this[GetIndex(key)].Value = value; }
        }

        /// <summary>
        /// Populates this Parameter List instance using the public properties of the T obj.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="obj">The obj.</param>
        private void Populate<T>(T obj)
        {
            foreach (KeyValuePair<string, object> pair in new ObjectProperties(obj, BindingFlags.Public | BindingFlags.Instance))
                Add(pair.Key, pair.Value);
        }
    }

    /// <summary>
    /// DynamicBuilder
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class DynamicBuilder<T>
    {
        private static readonly MethodInfo getValueMethod = typeof(IDataRecord).GetMethod("get_Item", new[] { typeof(int) });
        private static readonly MethodInfo isDBNullMethod = typeof(IDataRecord).GetMethod("IsDBNull", new[] { typeof(int) });
        private delegate T Load(IDataRecord dataRecord);
        private Load handler;

        /// <summary>
        /// Initializes a new instance of the <see cref="DynamicBuilder&lt;T&gt;"/> class.
        /// </summary>
        private DynamicBuilder() { }

        /// <summary>
        /// Builds a T object using the specified data record.
        /// </summary>
        /// <param name="dataRecord">The data record.</param>
        /// <returns></returns>
        public T Build(IDataRecord dataRecord)
        {
            return handler(dataRecord);
        }

        /// <summary>
        /// Creates the builder.
        /// </summary>
        /// <param name="dataRecord">The data record.</param>
        /// <returns></returns>
        public static DynamicBuilder<T> CreateBuilder(IDataRecord dataRecord)
        {
            DynamicBuilder<T> DynamicBuilder = new DynamicBuilder<T>();

            DynamicMethod method = new DynamicMethod("DynamicCreate", typeof(T), new[] { typeof(IDataRecord) }, typeof(T), true);
            ILGenerator generator = method.GetILGenerator();

            LocalBuilder result = generator.DeclareLocal(typeof(T));
            generator.Emit(OpCodes.Newobj, typeof(T).GetConstructor(Type.EmptyTypes));
            generator.Emit(OpCodes.Stloc, result);

            for (int i = 0; i < dataRecord.FieldCount; i++)
            {
                PropertyInfo propertyInfo = typeof(T).GetProperty(dataRecord.GetName(i),
                                           BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase |
                                           BindingFlags.FlattenHierarchy | BindingFlags.Default);
                Label endIfLabel = generator.DefineLabel();

                if (propertyInfo != null && propertyInfo.GetSetMethod() != null)
                {
                    generator.Emit(OpCodes.Ldarg_0);
                    generator.Emit(OpCodes.Ldc_I4, i);
                    generator.Emit(OpCodes.Callvirt, isDBNullMethod);
                    generator.Emit(OpCodes.Brtrue, endIfLabel);

                    generator.Emit(OpCodes.Ldloc, result);
                    generator.Emit(OpCodes.Ldarg_0);
                    generator.Emit(OpCodes.Ldc_I4, i);
                    generator.Emit(OpCodes.Callvirt, getValueMethod);

                    Type dataType = (propertyInfo.PropertyType.Name.ToLower().Contains("nullable"))
                        ? getNullableType(dataRecord.GetFieldType(i))
                        : dataRecord.GetFieldType(i);

                    generator.Emit(OpCodes.Unbox_Any, dataType);

                    generator.Emit(OpCodes.Callvirt, propertyInfo.GetSetMethod());

                    generator.MarkLabel(endIfLabel);
                }
            }

            generator.Emit(OpCodes.Ldloc, result);
            generator.Emit(OpCodes.Ret);

            DynamicBuilder.handler = (Load)method.CreateDelegate(typeof(Load));
            return DynamicBuilder;
        }

        /// <summary>
        /// Gets the type of the nullable.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        private static Type getNullableType(Type type)
        {
            Type result = null;

            if (type == typeof(bool))
                result = typeof(bool?);

            if (type == typeof(byte))
                result = typeof(byte?);

            if (type == typeof(DateTime))
                result = typeof(DateTime?);

            if (type == typeof(decimal))
                result = typeof(decimal?);

            if (type == typeof(double))
                result = typeof(double?);

            if (type == typeof(float))
                result = typeof(float?);

            if (type == typeof(Guid))
                result = typeof(Guid?);

            if (type == typeof(Int16))
                result = typeof(Int16?);

            if (type == typeof(Int32))
                result = typeof(Int32?);

            if (type == typeof(Int64))
                result = typeof(Int64?);

            return result;
        }
    }

    /// <summary>
    /// ObjectProperties
    /// </summary>
    public class ObjectProperties : Dictionary<string, object>
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="ObjectProperties"/> class.
        /// </summary>
        /// <param name="o">The o.</param>
        public ObjectProperties(object o)
        {
            foreach (PropertyInfo propertyInfo in o.GetType().GetProperties())
                try { Add(propertyInfo.Name, propertyInfo.GetValue(o, null)); }
                catch { { Add(propertyInfo.Name, null); } }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ObjectProperties"/> class.
        /// </summary>
        /// <param name="o">The o.</param>
        /// <param name="bindingFlags">The binding flags.</param>
        public ObjectProperties(object o, BindingFlags bindingFlags)
        {
            foreach (PropertyInfo propertyInfo in o.GetType().GetProperties(bindingFlags))
                try { Add(propertyInfo.Name, propertyInfo.GetValue(o, null)); }
                catch { { Add(propertyInfo.Name, null); } }
        }
    }
}
